﻿using UnityEngine;
using System.Collections;

namespace RootMotion.FinalIK
{

    /// <summary>
    /// Procedural recoil using FBBIK.
    /// </summary>
    public class Recoil : OffsetModifier
    {

        [System.Serializable]
        public class RecoilOffset
        {

            [Tooltip("Offset vector for the associated effector when doing recoil.")]
            public Vector3 offset;
            [Tooltip("When firing before the last recoil has faded, how much of the current recoil offset will be maintained?")]
            [Range(0f, 1f)] public float additivity = 1f;
            [Tooltip("Max additive recoil for automatic fire.")]
            public float maxAdditiveOffsetMag = 0.2f;

            // Linking this to an effector
            [System.Serializable]
            public class EffectorLink
            {
                [Tooltip("Type of the FBBIK effector to use")]
                public FullBodyBipedEffector effector;
                [Tooltip("Weight of using this effector")]
                public float weight;
            }

            [Tooltip("Linking this recoil offset to FBBIK effectors.")]
            public EffectorLink[] effectorLinks;

            private Vector3 additiveOffset;
            private Vector3 lastOffset;

            // Start recoil
            public void Start()
            {
                if (additivity <= 0f) return;

                additiveOffset = Vector3.ClampMagnitude(lastOffset * additivity, maxAdditiveOffsetMag);
            }

            // Apply offset to FBBIK effectors
            public void Apply(IKSolverFullBodyBiped solver, Quaternion rotation, float masterWeight, float length, float timeLeft)
            {
                additiveOffset = Vector3.Lerp(Vector3.zero, additiveOffset, timeLeft / length);
                lastOffset = (rotation * (offset * masterWeight)) + (rotation * additiveOffset);

                foreach (EffectorLink e in effectorLinks)
                {
                    solver.GetEffector(e.effector).positionOffset += lastOffset * e.weight;
                }
            }
        }

        [System.Serializable]
        public enum Handedness
        {
            Right,
            Left
        }

        [Tooltip("Reference to the AimIK component. Optional, only used to getting the aiming direction.")]
        public AimIK aimIK;
        [Tooltip("Optional head AimIK solver. This solver should only use neck and head bones and have the head as Aim Transform")]
        public AimIK headIK;
        [Tooltip("Set this true if you are using IKExecutionOrder.cs or a custom script to force AimIK solve after FBBIK.")]
        public bool aimIKSolvedLast;
        [Tooltip("Which hand is holding the weapon?")]
        public Handedness handedness;
        [Tooltip("Check for 2-handed weapons.")]
        public bool twoHanded = true;
        [Tooltip("Weight curve for the recoil offsets. Recoil procedure is as long as this curve.")]
        public AnimationCurve recoilWeight;
        [Tooltip("How much is the magnitude randomized each time Recoil is called?")]
        public float magnitudeRandom = 0.1f;
        [Tooltip("How much is the rotation randomized each time Recoil is called?")]
        public Vector3 rotationRandom;
        [Tooltip("Rotating the primary hand bone for the recoil (in local space).")]
        public Vector3 handRotationOffset;
        [Tooltip("Time of blending in another recoil when doing automatic fire.")]
        public float blendTime;

        [Space(10)]

        [Tooltip("FBBIK effector position offsets for the recoil (in aiming direction space).")]
        public RecoilOffset[] offsets;

        [HideInInspector] public Quaternion rotationOffset = Quaternion.identity;

        private float magnitudeMlp = 1f;
        private float endTime = -1f;
        private Quaternion handRotation, secondaryHandRelativeRotation, randomRotation;
        private float length = 1f;
        private bool initiated;
        private float blendWeight;
        private float w;
        private Quaternion primaryHandRotation = Quaternion.identity;
        //private Quaternion secondaryHandRotation = Quaternion.identity;
        private bool handRotationsSet;
        private Vector3 aimIKAxis;

        /// <summary>
        /// Returns true if recoil has finished or has not been called at all.
        /// </summary>
        public bool isFinished
        {
            get
            {
                return Time.time > endTime;
            }
        }

        /// <summary>
        /// Sets the starting rotations for the hands for 1 frame. Use this if the final rotation of the hands will not be the same as before FBBIK solves.
        /// </summary>
        public void SetHandRotations(Quaternion leftHandRotation, Quaternion rightHandRotation)
        {
            if (handedness == Handedness.Left)
            {
                primaryHandRotation = leftHandRotation;
                //secondaryHandRotation = rightHandRotation;
            }
            else
            {
                primaryHandRotation = rightHandRotation;
                //secondaryHandRotation = leftHandRotation;
            }

            handRotationsSet = true;
        }

        /// <summary>
        /// Starts the recoil procedure.
        /// </summary>
        public void Fire(float magnitude)
        {
            float rnd = magnitude * UnityEngine.Random.value * magnitudeRandom;
            magnitudeMlp = magnitude + rnd;

            randomRotation = Quaternion.Euler(rotationRandom * UnityEngine.Random.value);

            foreach (RecoilOffset offset in offsets)
            {
                offset.Start();
            }

            if (Time.time < endTime) blendWeight = 0f;
            else blendWeight = 1f;

            Keyframe[] keys = recoilWeight.keys;
            length = keys[keys.Length - 1].time;
            endTime = Time.time + length;
        }

        protected override void OnModifyOffset()
        {
            if (aimIK != null) aimIKAxis = aimIK.solver.axis;

            if (!initiated && ik != null)
            {
                initiated = true;
                if (headIK != null) headIK.enabled = false;
                ik.solver.OnPostUpdate += AfterFBBIK;
                if (aimIK != null) aimIK.solver.OnPostUpdate += AfterAimIK;
            }

            if (Time.time >= endTime)
            {
                rotationOffset = Quaternion.identity;
                return;
            }

            blendTime = Mathf.Max(blendTime, 0f);
            if (blendTime > 0f) blendWeight = Mathf.Min(blendWeight + Time.deltaTime * (1f / blendTime), 1f);
            else blendWeight = 1f;

            // Current weight of offset
            float wTarget = recoilWeight.Evaluate(length - (endTime - Time.time)) * magnitudeMlp;
            w = Mathf.Lerp(w, wTarget, blendWeight);

            // Find the rotation space of the recoil
            Quaternion lookRotation = aimIK != null && aimIK.solver.transform != null && !aimIKSolvedLast ? Quaternion.LookRotation(aimIK.solver.IKPosition - aimIK.solver.transform.position, ik.references.root.up) : ik.references.root.rotation;
            lookRotation = randomRotation * lookRotation;

            // Apply FBBIK effector positionOffsets
            foreach (RecoilOffset offset in offsets)
            {
                offset.Apply(ik.solver, lookRotation, w, length, endTime - Time.time);
            }

            if (!handRotationsSet)
            {
                primaryHandRotation = primaryHand.rotation;
                //if (twoHanded) secondaryHandRotation = secondaryHand.rotation;
            }
            handRotationsSet = false;

            // Rotation offset of the primary hand
            rotationOffset = Quaternion.Lerp(Quaternion.identity, Quaternion.Euler(randomRotation * primaryHandRotation * handRotationOffset), w);
            handRotation = rotationOffset * primaryHandRotation;

            // Fix the secondary hand relative to the primary hand
            if (twoHanded)
            {
                Vector3 secondaryHandRelativePosition = Quaternion.Inverse(primaryHand.rotation) * (secondaryHand.position - primaryHand.position);
                secondaryHandRelativeRotation = Quaternion.Inverse(primaryHand.rotation) * secondaryHand.rotation;

                Vector3 primaryHandPosition = primaryHand.position + primaryHandEffector.positionOffset;
                Vector3 secondaryHandPosition = primaryHandPosition + handRotation * secondaryHandRelativePosition;

                secondaryHandEffector.positionOffset += secondaryHandPosition - (secondaryHand.position + secondaryHandEffector.positionOffset);
            }

            if (aimIK != null && aimIKSolvedLast) aimIK.solver.axis = Quaternion.Inverse(ik.references.root.rotation) * Quaternion.Inverse(rotationOffset) * aimIKAxis;
        }

        private void AfterFBBIK()
        {
            if (Time.time < endTime)
            {
                // Rotate the hand bones
                primaryHand.rotation = handRotation;
                if (twoHanded) secondaryHand.rotation = primaryHand.rotation * secondaryHandRelativeRotation;
            }

            if (!aimIKSolvedLast && headIK != null) headIK.solver.Update();
        }

        private void AfterAimIK()
        {
            if (aimIKSolvedLast) aimIK.solver.axis = aimIKAxis;
            if (aimIKSolvedLast && headIK != null) headIK.solver.Update();
        }

        // Shortcuts
        private IKEffector primaryHandEffector
        {
            get
            {
                if (handedness == Handedness.Right) return ik.solver.rightHandEffector;
                return ik.solver.leftHandEffector;
            }
        }

        private IKEffector secondaryHandEffector
        {
            get
            {
                if (handedness == Handedness.Right) return ik.solver.leftHandEffector;
                return ik.solver.rightHandEffector;
            }
        }

        private Transform primaryHand
        {
            get
            {
                return primaryHandEffector.bone;
            }
        }

        private Transform secondaryHand
        {
            get
            {
                return secondaryHandEffector.bone;
            }
        }

        protected override void OnDestroy()
        {
            base.OnDestroy();
            if (ik != null && initiated)
            {
                ik.solver.OnPostUpdate -= AfterFBBIK;
                if (aimIK != null) aimIK.solver.OnPostUpdate -= AfterAimIK;
            }
        }

    }
}
